﻿//-----------------------------------------------------------------------------------------------------------
// Wbfs Engine (WBFSe) Project by Omega Frost 
// http://wbfse.codeplex.com/
//
// WBFSe is Licensed under the terms of the 
// Microsoft Reciprocal License (Ms-RL)
//-----------------------------------------------------------------------------------------------------------
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using WBFSe.Attributes;
using WBFSe.Data;
using WBFSe.IO;
using System.Collections.Generic;

namespace WBFSe.Builders.V1
{
    /// <summary>
    /// Builder for Wii discs
    /// </summary>
    class BuilderV1 : IDiscBuilder
    {
        // Variables

        private IWbfsStream stream;
        private UnmanagedArray memory;
        private Byte[] buffer;

        private Rijndael aesDecryptor;

        private uint magic1;
        private uint magic2;

        private String name;
        private String code;
        private Region region;

        private long length;

        private bool doubleLayer;
        private bool hasUpdate;
        private bool hasChannel;

        private Boolean[] wiiSectorUsage;
        private UsageTable usageTable;
        
        private int partitionCount;
        private long partitionInfoTablePosition;
        
        private PartitionInfoV1[] partitions;
        private PartitionDataV1 currentPartition;

        private FileSystemNode root;

        private BuildFlags flags;

        private List<FixData> fixData;

        // Properties

        /// <summary>
        /// Name of the game
        /// </summary>
        public String Name
        {
            get { return this.name; }
        }

        /// <summary>
        /// Code of the game
        /// </summary>
        public String Code
        {
            get { return this.code; }
        }

        /// <summary>
        /// Region of the game
        /// </summary>
        public Region Region
        {
            get { return this.region; }
        }

        /// <summary>
        /// Type of the game
        /// </summary>
        public GameType Type
        {
            get { return GameType.Wii; }
        }

        /// <summary>
        /// Length of the game
        /// </summary>
        public Int64 Length
        {
            get { return this.length; }
        }

        /// <summary>
        /// Double layer game
        /// </summary>
        public Boolean DoubleLayer
        {
            get { return this.doubleLayer; }
        }

        /// <summary>
        /// Update installer
        /// </summary>
        public Boolean HasUpdate
        {
            get { return this.hasUpdate; }
        }

        /// <summary>
        /// Channel installer
        /// </summary>
        public Boolean HasChannel
        {
            get { return this.hasChannel; }
        }

        /// <summary>
        /// Sector by sector usage
        /// </summary>
        public UsageTable Usage
        {
            get { return this.usageTable; }
        }

        /// <summary>
        /// Disc sector size
        /// </summary>
        public Int32 SectorSize
        {
            get { return WbfsConstantsV1.WiiSectorSize; }
        }

        /// <summary>
        /// Filesystem's root
        /// </summary>
        public FileSystemNode Root
        {
            get { return this.root; }
        }

        /// <summary>
        /// The flags used to build the disc
        /// </summary>
        public BuildFlags BuildFlags
        {
            get { return this.flags; }
        }

        /// <summary>
        /// List of modified regions of the disc
        /// </summary>
        public IList<FixData> FixData
        {
            get { return this.fixData.AsReadOnly(); }
        }

        // Methods

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="stream">Disc stream</param>
        public BuilderV1(IWbfsStream stream)
        {
            this.stream = stream;
            this.memory = new UnmanagedArray(WbfsConstantsV1.WiiSectorSize);
            this.buffer = new Byte[WbfsConstantsV1.WiiSectorSize];

            Reset();
        }

        /// <summary>
        /// Sets all the values to default
        /// </summary>
        private void Reset()
        {
            this.aesDecryptor = null;

            this.magic1 = 0;
            this.magic2 = 0;

            this.name = String.Empty;
            this.code = String.Empty;
            this.region = Region.Unknown;

            this.length = 0;

            this.doubleLayer = false;
            this.hasUpdate = false;
            this.hasChannel = false;

            this.usageTable = null;

            this.wiiSectorUsage = new Boolean[WbfsConstantsV1.WiiSectorsPerDisc];
            Array.Clear(this.wiiSectorUsage, 0, this.wiiSectorUsage.Length);

            this.partitionCount = 0;
            this.partitionInfoTablePosition = 0;
            this.currentPartition = null;
            this.partitions = null;

            this.fixData = new List<FixData>();
        }

        /// <summary>
        /// Builds the disc
        /// </summary>
        /// <param name="flags">Flags</param>
        /// <returns>True for successful build, false if not a valid disc</returns>
        public bool Build(BuildFlags flags)
        {
            this.flags = flags;

            Reset();

            if (!ParseHeader())
                return false;

            ParsePartitionTable();
            ParseRegionData();
            ParseSecondMagic();

            if (flags.HasFlag(BuildFlags.UpdateHeaderOnly))
                return true;

            if (flags.HasFlag(BuildFlags.MapFilesystem))
                this.root = new FileSystemNode("ROOT");

            for (int i = 0; i < this.partitionCount; i++)
            {
                bool fullpart = false;

                // Skip the channel installer... until i understand it...
                if (this.partitions[i].Type == PartitionType.ChannelInstaler)
                {
                    if (flags.HasFlag(BuildFlags.RemoveChannel))
                        continue;
                    
                    fullpart = true;
                }
                else if (this.partitions[i].Type == PartitionType.Update)
                {
                    if (flags.HasFlag(BuildFlags.RemoveUpdate))
                        continue;

                    fullpart = true;
                }

                // Prepare a new partition
                this.currentPartition = new PartitionDataV1();
                this.currentPartition.Info = this.partitions[i];

                // Parse the patition
                if (flags.HasFlag(BuildFlags.MapFilesystem))
                {
                    ParsePartition(true, flags.HasFlag(BuildFlags.KeepCryptData), fullpart);
                    this.root.AddNode(this.currentPartition.root);
                }
                else
                {
                    ParsePartition(false, flags.HasFlag(BuildFlags.KeepCryptData), fullpart);
                }

                if (!flags.HasFlag(BuildFlags.KeepCryptData))
                    this.currentPartition = null;
            }

            int lastSector = GetLastUsedWiiSector();
            if (lastSector > WbfsConstantsV1.WiiSectorsPerDisc / 2)
                this.doubleLayer = true;

            this.length = (long)lastSector * WbfsConstantsV1.WiiSectorSize;
            this.usageTable = new UsageTable(this.wiiSectorUsage);
            this.wiiSectorUsage = null;

            // Fix

            if ((this.hasUpdate && flags.HasFlag(BuildFlags.RemoveUpdate)) ||
                (this.hasChannel && flags.HasFlag(BuildFlags.RemoveChannel)))
            {
                Byte[] table;
                FixPartitionTable(out table);

                FixData fixTable = new FixData(WbfsConstantsV1.WiiPartitionTablePos, table);
                this.fixData.Add(fixTable);
            }

            // Cleanup

            for (int i = 0; i < this.partitions.Length; i++)
                this.partitions[i] = null;

            this.partitions = null;

            return true;
        }

        /// <summary>
        /// Extracts the content of a file in the disc
        /// </summary>
        /// <param name="node">File</param>
        /// <param name="stream">Output stream</param>
        public bool ExtractFile(FileSystemNode node, Stream stream)
        {
            if (node.Attributes.HasFlag(FileSystemAttributes.Directory))
                return false;

            // Minimal variables to make the algotithm work
            this.wiiSectorUsage = new Boolean[WbfsConstantsV1.WiiSectorsPerDisc];
            Array.Clear(this.wiiSectorUsage, 0, this.wiiSectorUsage.Length);

            ParsePartitionTable();

            // Prepare a new partition
            this.currentPartition = new PartitionDataV1();
            this.currentPartition.Info = this.partitions[node.Partition];

            // Parse the patition
            ParsePartition(false, true, false);

            // Reads the data
            if (!node.Attributes.HasFlag(FileSystemAttributes.System))
            {
                this.ReadPartitionData(stream, node.PartitionOffset, (int)node.Size);
            }
            else
            {
                if (node.Attributes.HasFlag(FileSystemAttributes.Encrypted))
                {
                    this.ReadPartitionData(stream, node.PartitionOffset, (int)node.Size);
                }
                else
                {
                    this.ReadPartition(stream, node.PartitionOffset, (int)node.Size);
                }
            }

            // Cleanup
            this.aesDecryptor.Clear();
            this.aesDecryptor = null;
            this.currentPartition.Ticket = null;
            this.currentPartition.TMD = null;
            this.currentPartition.Cert = null;
            this.currentPartition.AplHeader = null;
            this.currentPartition.Fst = null;
            this.currentPartition = null;

            for (int i = 0; i < this.partitions.Length; i++)
                this.partitions[i] = null;

            this.partitions = null;
            this.wiiSectorUsage = null;
            
            return true;
        }

        /// <summary>
        /// Parses the disc's main header
        /// </summary>
        /// <returns>True if the magic code matches</returns>
        private bool ParseHeader()
        {
            ReadDiscSector(0);
            
            this.code = this.ReadString(WbfsConstantsV1.WiiCodePos, WbfsConstantsV1.WiiCodeLen);
            this.name = this.ReadString(WbfsConstantsV1.WiiNamePos, WbfsConstantsV1.WiiNameLen);
            this.region = this.GetRegionFromCode(this.code);

            this.magic1 = this.ReadUInt32(WbfsConstantsV1.WiiMagic1Pos);
            if (this.magic1 != WbfsConstantsV1.WiiMagic1) return false;

            return true;
        }

        /// <summary>
        /// Parses the disc's partition table
        /// </summary>
        private void ParsePartitionTable()
        {
            ReadDiscSector(WbfsConstantsV1.WiiPartitionTablePos);

            this.partitionCount = this.ReadInt32(0);
            this.partitionInfoTablePosition = (long)this.ReadInt32(4) << 2;
            this.partitions = new PartitionInfoV1[this.partitionCount];

            int offset = (int)(this.partitionInfoTablePosition -
                WbfsConstantsV1.WiiPartitionTablePos);

            for (int i = 0; i < this.partitionCount; i++)
            {
                this.partitions[i] = new PartitionInfoV1();
                this.partitions[i].Position = (long)this.ReadInt32(8 * i + offset) << 2;
                this.partitions[i].Type = (PartitionType)this.ReadInt32(8 * i + 4 + offset);
                this.partitions[i].Index = i;

                switch (this.partitions[i].Type)
                {
                    case PartitionType.Update:
                        this.hasUpdate = true;
                        break;
                    case PartitionType.ChannelInstaler:
                        this.hasChannel = true;
                        break;
                }
            }
        }

        /// <summary>
        /// Rebuilds the partition table area removing unused partitions
        /// </summary>
        /// <returns></returns>
        private void FixPartitionTable(out byte[] table)
        {
            table = new Byte[WbfsConstantsV1.WiiSectorSize];
            int offset = (int)(this.partitionInfoTablePosition -
                WbfsConstantsV1.WiiPartitionTablePos);

            int n = 0;
            foreach (PartitionInfoV1 p in this.partitions)
            {
                switch (p.Type)
                {
                    case PartitionType.ChannelInstaler:
                        if (flags.HasFlag(BuildFlags.RemoveChannel))
                            continue;
                        break;

                    case PartitionType.Update:
                        if (flags.HasFlag(BuildFlags.RemoveChannel))
                            continue;
                        break;
                }

                n++;
            }

            this.WriteInt32(table, 0, n);
            this.WriteInt32(table, 4, (int)(this.partitionInfoTablePosition >> 2));

            // Partition table 1

            foreach (PartitionInfoV1 p in this.partitions)
            {
                switch (p.Type)
                {
                    case PartitionType.ChannelInstaler:
                        if (flags.HasFlag(BuildFlags.RemoveChannel))
                            continue;
                        break;

                    case PartitionType.Update:
                        if (flags.HasFlag(BuildFlags.RemoveChannel))
                            continue;
                        break;
                }

                this.WriteInt32(table, 8 * n + offset, (int)(p.Position >> 2));
                this.WriteInt32(table, 8 * n + 4 + offset, (int)p.Type);

                n++;
            }
        }

        /// <summary>
        /// Parses the disc region codes
        /// </summary>
        private void ParseRegionData()
        {
            ReadDiscSector(0x4E000);
        }

        /// <summary>
        /// Parses the second verification code
        /// </summary>
        /// <returns>True on matchS</returns>
        private bool ParseSecondMagic()
        {
            int pos = (int)((uint)WbfsConstantsV1.WiiMagic2Pos & (0xFFFFFFFFU <<
                WbfsConstantsV1.WiiSectorSizeLSh));
            
            int off = 0x4FFFC - pos;

            ReadDiscSector(pos);

            this.magic2 = this.ReadUInt32(off);
            if (this.magic2 != 0xC3F81A8E)
                return false;
            else
                return true;
        }

        /// <summary>
        /// Parses a Disc partition
        /// <param name="mapfs">Maps the filesystem</param>
        /// </summary>
        private void ParsePartition(bool mapfs, bool keepdata, bool fullpart)
        {
            ReadPartitionSector(0);

            // Ticket
            this.currentPartition.Ticket = ReadBytes(0, 0x2A4);

            // TMD
            this.currentPartition.TMDSize = this.ReadInt32(0x2A4);
            this.currentPartition.TMDOffset = (long)this.ReadInt32(0x2A8) << 2;

            // Cert
            this.currentPartition.CertChainSize = this.ReadInt32(0x2AC);
            this.currentPartition.CertChainOffset = (long)this.ReadInt32(0x2B0) << 2;

            // H3
            this.currentPartition.H3TableOffset = (long)this.ReadInt32(0x2B4) << 2;

            // Data
            this.currentPartition.DataOffset = (long)this.ReadInt32(0x2B8) << 2;
            this.currentPartition.DataSize = (long)this.ReadInt32(0x2BC) << 2;
            this.currentPartition.DataStartBlock = (int)((this.currentPartition.Info.Position +
                this.currentPartition.DataOffset) >> WbfsConstantsV1.WiiSectorSizeLSh);

            // TMD
            this.currentPartition.TMD = new byte[this.currentPartition.TMDSize];
            ReadPartition(this.currentPartition.TMD, 
                this.currentPartition.TMDOffset, 
                this.currentPartition.TMDSize);

            // Cert
            this.currentPartition.Cert = new byte[this.currentPartition.CertChainSize];
            ReadPartition(this.currentPartition.Cert,
                this.currentPartition.CertChainOffset,
                this.currentPartition.CertChainSize);

            if (fullpart)
            {
                MarkUsedPartitionDataSectors(0,
                    this.currentPartition.DataSize);
            }

            // Title Key
            ParseTitleKey();

            // Mark the H3 table as used
            MarkUsedPartitionSectors(this.currentPartition.H3TableOffset, 0x18000);

            // Read the first encrypted block of the partition
            ReadCiphedPartitionSector(this.currentPartition.DataOffset);

            this.currentPartition.DolOffset = this.ReadInt32(0x420) << 2;
            this.currentPartition.FstOffset = this.ReadInt32(0x424) << 2;
            this.currentPartition.FstSize = this.ReadInt32(0x428) << 2;
            this.currentPartition.AplOffset = 0x2440;

            this.currentPartition.AplHeader = new byte[0x20];
            ReadPartitionData(this.currentPartition.AplHeader,
                this.currentPartition.AplOffset, 0x20);

            this.currentPartition.AplSize = 0x20 +
                this.ReadInt32(this.currentPartition.AplHeader, 0x14) +
                this.ReadInt32(this.currentPartition.AplHeader, 0x18);

            // Mark the APL and DOL blocks
            MarkUsedPartitionDataSectors(
                this.currentPartition.AplOffset,
                this.currentPartition.AplSize);

            MarkUsedPartitionDataSectors(
                this.currentPartition.DolOffset,
                this.currentPartition.FstOffset -
                this.currentPartition.DolOffset);

            // Read the FST
            this.currentPartition.Fst = new byte[this.currentPartition.FstSize];
            ReadPartitionData(this.currentPartition.Fst,
                this.currentPartition.FstOffset,
                this.currentPartition.FstSize);

            this.currentPartition.FstItemsCount =
                this.ReadInt32(this.currentPartition.Fst, 0x8);

            this.currentPartition.FstNameTablePos =
                0x0C * this.currentPartition.FstItemsCount; // 12 bytes per file

            // Parse the files
            if (this.currentPartition.FstItemsCount > 1) // item 0 is the FS root
            {
                if (!mapfs)
                {
                    ParseFileSystem();
                }
                else
                {
                    int offset = 0;
                    this.currentPartition.root = 
                        MapFileSystem(ref offset);

                    // Create the system files
                    FileSystemNode item, part = new FileSystemNode(".partition");
                    part.SetFileAttributes(this.currentPartition.Info.Index, 
                        0, 1000, true, false);

                    // Ticket
                    item = new FileSystemNode("Ticket.bin");
                    item.SetFileAttributes(this.currentPartition.Info.Index, 
                        0, 0x2A4, true, false);
                    
                    part.AddNode(item);

                    // Tmd
                    item = new FileSystemNode("Tmd.bin");
                    item.SetFileAttributes(this.currentPartition.Info.Index, 
                        this.currentPartition.TMDOffset,
                        this.currentPartition.TMDSize, true, false);

                    part.AddNode(item);

                    // Cert
                    item = new FileSystemNode("Cert.bin");
                    item.SetFileAttributes(this.currentPartition.Info.Index, 
                        this.currentPartition.CertChainOffset,
                        this.currentPartition.CertChainSize, true, false);

                    part.AddNode(item);

                    // H3
                    item = new FileSystemNode("H3.bin");
                    item.SetFileAttributes(this.currentPartition.Info.Index,
                        this.currentPartition.H3TableOffset, 0x18000, true, false);

                    part.AddNode(item);

                    // Dol
                    item = new FileSystemNode("main.dol");
                    item.SetFileAttributes(this.currentPartition.Info.Index,
                        this.currentPartition.DolOffset,
                        this.currentPartition.FstOffset -
                        this.currentPartition.DolOffset, true, true);

                    part.AddNode(item);

                    // APL
                    item = new FileSystemNode("apploader.img");
                    item.SetFileAttributes(this.currentPartition.Info.Index,
                        this.currentPartition.AplOffset,
                        this.currentPartition.AplSize, true, true);

                    part.AddNode(item);

                    // FST
                    item = new FileSystemNode("fst.bin");
                    item.SetFileAttributes(this.currentPartition.Info.Index,
                        this.currentPartition.FstOffset,
                        this.currentPartition.FstSize, true, true);

                    part.AddNode(item);

                    this.currentPartition.root.AddNode(part);
                }
            }

            if (!keepdata)
            {
                // Cleanup
                this.aesDecryptor.Clear();
                this.aesDecryptor = null;
                this.currentPartition.Ticket = null;
                this.currentPartition.TMD = null;
                this.currentPartition.Cert = null;
                this.currentPartition.AplHeader = null;
                this.currentPartition.Fst = null;
            }
        }

        /// <summary>
        /// Decripts the title key
        /// </summary>
        private void ParseTitleKey()
        {
            //Copia a chave a partir do Ticket
            byte[] titlekey = new byte[0x10];
            Array.Copy(this.currentPartition.Ticket, 0x01bf, titlekey, 0, 0x10);

            //Copia o vetor de inicializaçao a partir do Ticket
            byte[] iv = new byte[0x10];
            Array.Copy(this.currentPartition.Ticket, 0x1dc, iv, 0, 8);

            //Aloca o vetor da chave
            this.currentPartition.TitleKey = new byte[0x10];

            //Cria e configura o decriptador
            this.aesDecryptor = RijndaelManaged.Create();
            this.aesDecryptor.BlockSize = 128;
            this.aesDecryptor.Mode = CipherMode.CBC;
            this.aesDecryptor.Padding = PaddingMode.Zeros;
            this.aesDecryptor.Key = WbfsConstantsV1.DiscCommonKey;
            this.aesDecryptor.IV = iv;

            //Decriptografa a chave
            ICryptoTransform aesDecryptor = this.aesDecryptor.CreateDecryptor();
            aesDecryptor.TransformBlock(titlekey, 0, 16, this.currentPartition.TitleKey, 0);
            aesDecryptor = null;
        }

        /// <summary>
        /// Parses the disc's file system
        /// </summary>
        private void ParseFileSystem()
        {
            byte[] fst = this.currentPartition.Fst;

            long fileSize = 0;
            long fileOffset = 0;
            int offset = 0;

            //Ignore the first item
            for (int i = 1; i < this.currentPartition.FstItemsCount; i++)
            {
                // Advance the offset
                offset += 12;

                // Check files only
                if (fst[offset] != 0)
                    continue;

                // Retrieves the offset in partition and size of the file
                fileOffset = (long)ReadInt32(fst, offset + 0x04) << 2;
                fileSize = (long)ReadUInt32(fst, offset + 0x08);

                // Marks the region of the file as used
                MarkUsedPartitionDataSectors(fileOffset, fileSize);
            }
        }

        /// <summary>
        /// Maps the filesystem to a tree
        /// </summary>
        /// <param name="offset">FST item offset</param>
        /// <returns>Filesystem tree</returns>
        private FileSystemNode MapFileSystem(ref int offset)
        {
            // End of the FS
            if (offset == this.currentPartition.FstItemsCount * 12)
                return null;

            byte[] fst = this.currentPartition.Fst;

            int namepos;
            String name;

            // Get the name position in the table
            namepos = this.currentPartition.FstNameTablePos + 
                this.ReadInt32(fst, offset) & 0x00FFFFFF;

            // Extract the name
            if (offset != 0) name = this.ReadString(fst, (int)namepos, -1);
            else name = "Partition " + this.currentPartition.Info.Index.ToString();

            // Create the fs node
            FileSystemNode node = new FileSystemNode(name);

            // Parse the node
            if (fst[offset] != 0)
            {
                // Size of the tree's branch
                long size = (long)ReadUInt32(fst, offset + 0x08) * 12;

                // Move to the next item
                offset += 12;

                int files = 0;
                while (offset < size)
                {
                    // Map the tree branch
                    FileSystemNode child = MapFileSystem(ref offset);
                    node.AddNode(child);

                    files++;
                }

                // Set the attributes for the node
                node.SetFileAttributes(this.currentPartition.Info.Index, 
                    0, files, false, true);
            }
            else
            {
                // Retrieves the file position and size
                long fileOffset = (long)ReadInt32(fst, offset + 0x04) << 2;
                long fileSize = (long)ReadUInt32(fst, offset + 0x08);

                // Mark the region
                MarkUsedPartitionDataSectors(fileOffset, fileSize);

                // Set the attributes for the node
                node.SetFileAttributes(this.currentPartition.Info.Index,
                    fileOffset, fileSize, false, true);

                // Move to the next item
                offset += 12;
            }

            return node;
        }

        /// <summary>
        /// Sets a block as used
        /// </summary>
        /// <param name="position">Absolute position in the disc</param>
        private void MarkUsedSector(long position)
        {
            int sblock = (int)(position >> WbfsConstantsV1.WiiSectorSizeLSh);
            this.wiiSectorUsage[sblock] = true;
        }

        /// <summary>
        /// Sets blocks as used
        /// </summary>
        /// <param name="position">Absolute position in the disc</param>
        /// <param name="length">Length of the data block</param>
        private void MarkUsedSectors(long position, long length)
        {
            int sblock = (int)(position >> WbfsConstantsV1.WiiSectorSizeLSh);
            int eblock = (int)((position + length - 1) >> WbfsConstantsV1.WiiSectorSizeLSh) + 1;

            for (int i = sblock; i < eblock; i++)
                this.wiiSectorUsage[i] = true;
        }

        /// <summary>
        /// Sets partition blocks as used
        /// </summary>
        /// <param name="position">Relative real position in the current partition</param>
        /// <param name="length">Length of the data block</param>
        private void MarkUsedPartitionSectors(long position, long length)
        {
            MarkUsedSectors(this.currentPartition.Info.Position + position, length);
        }

        /// <summary>
        /// Sets partition's data blocks as used
        /// </summary>
        /// <param name="position">Relative effective position in the current partition's data offset</param>
        /// <param name="length">Effective length of the data block</param>
        private void MarkUsedPartitionDataSectors(long position, long length)
        {
            int pblock = this.currentPartition.DataStartBlock;
            int sblock = pblock + (int)(position / 0x7C00);
            int eblock = pblock + (int)((position + length - 1) / 0x7C00) + 1;

            for (int i = sblock; i < eblock; i++)
                this.wiiSectorUsage[i] = true;
        }

        /// <summary>
        /// Finds the last used partition block
        /// </summary>
        /// <returns>The last sector used</returns>
        private int GetLastUsedWiiSector()
        {
            for (int i = this.wiiSectorUsage.Length - 1; i >= 0; i--)
                if (this.wiiSectorUsage[i]) return i;

            return -1; //Should neve be reached
        }

        /// <summary>
        /// Reads a data block from the disc to the buffer
        /// </summary>
        /// <param name="position">Absolute position in the disc</param>
        private void ReadDiscSector(long position)
        {
            this.stream.Read(this.memory.DataPtr, 0, position, WbfsConstantsV1.WiiSectorSize);
            this.memory.CopyTo(this.buffer);

            MarkUsedSector(position);
        }

        /// <summary>
        /// Reads and decrypts a 32768 data block from the disc
        /// </summary>
        /// <param name="position">Absolute position in the disc</param>
        private void ReadCiphedDiscSector(long position)
        {
            this.stream.Read(this.memory.DataPtr, 0, position, WbfsConstantsV1.WiiSectorSize);
            this.memory.CopyTo(this.buffer);

            MarkUsedSector(position);

            byte[] iv = ReadBytes(0x3D0, 16);
            byte[] ciphed = ReadBytes(0x400, 0x7C00);

            this.aesDecryptor.Key = this.currentPartition.TitleKey;
            this.aesDecryptor.IV = iv;

            ICryptoTransform aesTransform = this.aesDecryptor.CreateDecryptor();
            aesTransform.TransformBlock(ciphed, 0, 0x7C00, this.buffer, 0);
        }

        /// <summary>
        /// Reads a data block from a partition
        /// </summary>
        /// <param name="position">Relative real position in the current partition</param>
        private void ReadPartitionSector(long position)
        {
            ReadDiscSector(position + this.currentPartition.Info.Position);
        }

        /// <summary>
        /// Reads and decrypts a 32768 data block from the disc
        /// </summary>
        /// <param name="position">Relative real position in the current partition</param>
        private void ReadCiphedPartitionSector(long position)
        {
            ReadCiphedDiscSector(position + this.currentPartition.Info.Position);
        }

        /// <summary>
        /// Reads raw data from the partition
        /// </summary>
        /// <param name="buffer">Output buffer</param>
        /// <param name="position">Relative effective position in the current partition's start</param>
        /// <param name="length">Absolute length of the data block</param>
        private void ReadPartition(Byte[] buffer, long position, int length)
        {
            position += this.currentPartition.Info.Position;

            int read = 0;
            int offset = (int)(position % WbfsConstantsV1.WiiSectorSize);
            position = (position / WbfsConstantsV1.WiiSectorSize) * 
                WbfsConstantsV1.WiiSectorSize;

            if (length > 0)
            {
                int toread = Math.Min(WbfsConstantsV1.WiiSectorSize - offset, length);

                ReadDiscSector(position);
                ReadBytes(buffer, 0, offset, toread);

                position += WbfsConstantsV1.WiiSectorSize;
                read += toread;
                length -= toread;
            }

            while (length >= WbfsConstantsV1.WiiSectorSize)
            {
                ReadDiscSector(position);
                ReadBytes(buffer, read, 0, WbfsConstantsV1.WiiSectorSize);

                position += WbfsConstantsV1.WiiSectorSize;
                read += WbfsConstantsV1.WiiSectorSize;
                length -= WbfsConstantsV1.WiiSectorSize;
            }

            if (length > 0)
            {
                ReadDiscSector(position);
                ReadBytes(buffer, read, 0, length);
            }
        }

        /// <summary>
        /// Reads raw data from the partition
        /// </summary>
        /// <param name="stream">Output stream</param>
        /// <param name="position">Relative effective position in the current partition's start</param>
        /// <param name="length">Absolute length of the data block</param>
        private void ReadPartition(Stream stream, long position, int length)
        {
            position += this.currentPartition.Info.Position;

            int read = 0;
            int offset = (int)(position % WbfsConstantsV1.WiiSectorSize);
            position = (position / WbfsConstantsV1.WiiSectorSize) *
                WbfsConstantsV1.WiiSectorSize;

            if (length > 0)
            {
                int toread = Math.Min(WbfsConstantsV1.WiiSectorSize - offset, length);

                ReadDiscSector(position);
                ReadBytes(stream, offset, toread);

                position += WbfsConstantsV1.WiiSectorSize;
                read += toread;
                length -= toread;
            }

            while (length >= WbfsConstantsV1.WiiSectorSize)
            {
                ReadDiscSector(position);
                ReadBytes(stream, 0, WbfsConstantsV1.WiiSectorSize);

                position += WbfsConstantsV1.WiiSectorSize;
                read += WbfsConstantsV1.WiiSectorSize;
                length -= WbfsConstantsV1.WiiSectorSize;
            }

            if (length > 0)
            {
                ReadDiscSector(position);
                ReadBytes(stream, 0, length);
            }
        }

        /// <summary>
        /// Reads data from the partition
        /// </summary>
        /// <param name="buffer">Output buffer</param>
        /// <param name="position">Relative effective position in the current partition's data offset</param>
        /// <param name="length">Effective length of the data block</param>
        private void ReadPartitionData(Byte[] buffer, long position, int length)
        {
            int read = 0;
            int offset = (int)(position % 0x7C00);
            position = (position / 0x7C00) * WbfsConstantsV1.WiiSectorSize;

            position += this.currentPartition.Info.Position +
                this.currentPartition.DataOffset;

            if(length > 0)
            {
                int toread = Math.Min(0x7C00 - offset, length);

                ReadCiphedDiscSector(position);
                ReadBytes(buffer, 0, offset, toread);

                position += WbfsConstantsV1.WiiSectorSize;
                read += toread;
                length -= toread;
            }

            while (length >= 0x7C00)
            {
                ReadCiphedDiscSector(position);
                ReadBytes(buffer, read, 0, WbfsConstantsV1.WiiSectorSize);

                position += WbfsConstantsV1.WiiSectorSize;
                read += WbfsConstantsV1.WiiSectorSize;
                length -= WbfsConstantsV1.WiiSectorSize;
            }

            if (length > 0)
            {
                ReadCiphedDiscSector(position);
                ReadBytes(buffer, read, 0, length);
            }
        }

        /// <summary>
        /// Reads data from the partition
        /// </summary>
        /// <param name="stream">Output stream</param>
        /// <param name="position">Relative effective position in the current partition's data offset</param>
        /// <param name="length">Effective length of the data block</param>
        private void ReadPartitionData(Stream stream, long position, int length)
        {
            int read = 0;
            int offset = (int)(position % 0x7C00);
            position = (position / 0x7C00) * WbfsConstantsV1.WiiSectorSize;

            position += this.currentPartition.Info.Position +
                this.currentPartition.DataOffset;

            if (length > 0)
            {
                int toread = Math.Min(0x7C00 - offset, length);

                ReadCiphedDiscSector(position);
                ReadBytes(stream, offset, toread);

                position += WbfsConstantsV1.WiiSectorSize;
                read += toread;
                length -= toread;
            }

            while (length >= 0x7C00)
            {
                ReadCiphedDiscSector(position);
                ReadBytes(stream, 0, WbfsConstantsV1.WiiSectorSize);

                position += WbfsConstantsV1.WiiSectorSize;
                read += WbfsConstantsV1.WiiSectorSize;
                length -= WbfsConstantsV1.WiiSectorSize;
            }

            if (length > 0)
            {
                ReadCiphedDiscSector(position);
                ReadBytes(stream, 0, length);
            }
        }

        /// <summary>
        /// Extracts the region information from a game code
        /// </summary>
        /// <param name="code">Game code</param>
        /// <returns>Region</returns>
        private Region GetRegionFromCode(string code)
        {
            return GetRegionFromCode((byte)(Char.ToLowerInvariant(code[3])));
        }

        /// <summary>
        /// Converts a region byte to a region code
        /// </summary>
        /// <param name="regionbyte">Region byte</param>
        /// <returns>Region</returns>
        private Region GetRegionFromCode(byte regionbyte)
        {
            //Convert to uppercase
            if (regionbyte > 90)
                regionbyte -= 32;

            switch (regionbyte)
            {
                case 0x41: return Region.RegionFree;
                case 0x43: return Region.Custom;
                case 0x44: return Region.PALGerman;
                case 0x45: return Region.NTSCU;
                case 0x46: return Region.PALFrench;
                case 0x4A: return Region.NTSCJ;
                case 0x4B: return Region.KOR;
                case 0x4C: return Region.PALWorld;
                case 0x50: return Region.PAL;
                case 0x51: return Region.KORJapanese;
                case 0x54: return Region.KOREnglish;
                case 0x58: return Region.PALOther;
                default: return Region.Unknown;
            }
        }

        /// <summary>
        /// Reads raw data from the main buffer
        /// </summary>
        /// <param name="offset">Offset in source buffer</param>
        /// <param name="length">Length the data</param>
        /// <returns>Raw data</returns>
        private Byte[] ReadBytes(int offset, int length)
        {
            Byte[] bytes = new Byte[length];
            Array.Copy(this.buffer, offset, bytes, 0, length);

            return bytes;
        }

        /// <summary>
        /// Reads raw data from the main buffer
        /// </summary>
        /// <param name="stream">Destination stream</param>
        /// <param name="offset">Offset in source buffer</param>
        /// <param name="length">Length the data</param>
        private void ReadBytes(Stream stream, int offset, int length)
        {
            stream.Write(this.buffer, offset, length);
        }

        /// <summary>
        /// Reads raw data from the main buffer
        /// </summary>
        /// <param name="buffer">Destination buffer</param>
        /// <param name="destOffset">Offset in destination buffer</param>
        /// <param name="offset">Offset in source buffer</param>
        /// <param name="length">Length the data</param>
        private void ReadBytes(Byte[] buffer, int destOffset, int offset, int length)
        {
            Array.Copy(this.buffer, offset, buffer, destOffset, length);
        }

        /// <summary>
        /// Reads a string from the main buffer
        /// </summary>
        /// <param name="offset">Offset in buffer</param>
        /// <param name="max">Maximum number of characters, set -1 to disable</param>
        /// <returns>String</returns>
        private string ReadString(int offset, int max)
        {
            StringBuilder builder;
            if (max <= 0) builder = new StringBuilder();
            else builder = new StringBuilder(max);

            while ((max > 0) || (max < 0))
            {
                if (this.buffer[offset] == 0)
                    break;

                builder.Append((char)this.buffer[offset]);

                offset++;
                max--;
            }

            return builder.ToString();
        }

        /// <summary>
        /// Reads a string from a buffer
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <param name="max">Maximum number of characters, set -1 to disable</param>
        /// <returns>String</returns>
        private string ReadString(byte[] buffer, int offset, int max)
        {
            StringBuilder builder;
            if (max <= 0) builder = new StringBuilder();
            else builder = new StringBuilder(max);

            while ((max > 0) || (max < 0))
            {
                if (buffer[offset] == 0)
                    break;

                builder.Append((char)buffer[offset]);

                offset++;
                max--;
            }

            return builder.ToString();
        }

        /// <summary>
        /// Reads an ushort and converts it from network to host
        /// </summary>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>UInt16</returns>
        private ushort ReadUInt16(int offset)
        {
            return (ushort)ReadInt16(offset);
        }

        /// <summary>
        /// Reads a short and convert it from network to host
        /// </summary>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>Int16</returns>
        private short ReadInt16(int offset)
        {
            return IPAddress.NetworkToHostOrder(BitConverter.ToInt16(this.buffer, offset));
        }

        /// <summary>
        /// Reads an uint and converts it from network to host
        /// </summary>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>UInt32</returns>
        private uint ReadUInt32(int offset)
        {
            return (uint)ReadInt32(offset);
        }

        /// <summary>
        /// Reads an int and convert it from network to host
        /// </summary>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>Int32</returns>
        private int ReadInt32(int offset)
        {
            return IPAddress.NetworkToHostOrder(BitConverter.ToInt32(this.buffer, offset));
        }

        /// <summary>
        /// Reads an ushort and converts it from network to host
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>UInt16</returns>
        private ushort ReadUInt16(byte[] buffer, int offset)
        {
            return (ushort)ReadInt16(buffer, offset);
        }

        /// <summary>
        /// Reads a short and converts it from network to host
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>Int16</returns>
        private short ReadInt16(byte[] buffer, int offset)
        {
            return IPAddress.NetworkToHostOrder(BitConverter.ToInt16(buffer, offset));
        }

        /// <summary>
        /// Reads an uint and converts it from network to host
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>UInt32</returns>
        private uint ReadUInt32(byte[] buffer, int offset)
        {
            return (uint)ReadInt32(buffer, offset);
        }

        /// <summary>
        /// Reads an int and converts it from network to host
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <returns>Int32</returns>
        private int ReadInt32(byte[] buffer, int offset)
        {
            return IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buffer, offset));
        }

        /// <summary>
        /// Converts an ushort from host to network and writes to a buffer
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <param name="val">Value</param>
        private void WriteUInt16(byte[] buffer, int offset, ushort val)
        {
            WriteInt16(buffer, offset, (short)val);
        }

        /// <summary>
        /// Converts a short from host to network and writes to a buffer
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <param name="val">Value</param>
        private void WriteInt16(byte[] buffer, int offset, short val)
        {
            BitConverter.GetBytes(IPAddress.HostToNetworkOrder(val)).CopyTo(buffer, offset);
        }

        /// <summary>
        /// Converts an uint from host to network and writes to a buffer
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <param name="val">Value</param>
        private void WriteUInt32(byte[] buffer, int offset, uint val)
        {
            WriteInt32(buffer, offset, (int)val);
        }

        /// <summary>
        /// Converts an int from host to network and writes to a buffer
        /// </summary>
        /// <param name="buffer">Destination buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <param name="val">Value</param>
        private void WriteInt32(byte[] buffer, int offset, int val)
        {
            BitConverter.GetBytes(IPAddress.HostToNetworkOrder(val)).CopyTo(buffer, offset);
        }

        /// <summary>
        /// Dispose the builder
        /// </summary>
        public void Dispose()
        {
            this.memory.Dispose();
        }
    }
}
